﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Scheme.Net.Runtime.AST;

namespace Scheme.Net.Runtime.Evaluator
{
    internal static class Primitives
    {
        public static IEvaluable UnspecifiedValue(IEnumerable<IEvaluable> x) { return Symbol.Unspecified; }
        public static IEvaluable UndefinedValue(IEnumerable<IEvaluable> x) { return Symbol.Undefined; }

        #region Predicates

        #region Type Predicates

        public static IEvaluable IsEnvironment(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(x.First() is Environment);
        }

        public static IEvaluable IsProcedure(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(x.First().IsTaggedList("PROCEDURE")
                    || x.First().IsTaggedList("PRIMITIVE")
                    || x.First().IsTaggedList("COMPILED-PROCEDURE"));
        }

        public static IEvaluable IsSymbol(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(x.First() is Symbol); ;
        }

        public static IEvaluable IsVector(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(x.First() is Vector);
        }

        public static IEvaluable IsNumber(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(x.First() is Runtime.AST.Number);
        }

        public static IEvaluable IsString(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(x.First() is Runtime.AST.String);
        }

        public static IEvaluable IsChar(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(x.First() is Runtime.AST.Character);
        }

        public static IEvaluable IsPair(IEnumerable<IEvaluable> x) 
        {
            if (IsProcedure(x).IsTrue()) return Symbol.False;

            return AST.Symbol.Boolean(x.First().IsPair()); 
        }

        public static IEvaluable IsInteger(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(IsNumber(x).IsTrue() && ((AST.Number)x.First()).Value is Scheme.Net.Integer);
        }

        public static IEvaluable IsReal(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(IsNumber(x).IsTrue() && ((AST.Number)x.First()).Value is Scheme.Net.Flonum);
        }

        public static IEvaluable IsRational(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(IsNumber(x).IsTrue() && ((AST.Number)x.First()).Value is Scheme.Net.Rational);
        }

        #endregion

        #region Generic Equality Predicates

        public static IEvaluable IsEq(IEnumerable<IEvaluable> x)
        {
            return AST.Symbol.Boolean(x.ElementAt(0) == x.ElementAt(1));
        }

        public static IEvaluable IsEqv(IEnumerable<IEvaluable> x)
        {
            var first = x.First();
            var second = x.Skip(1).First();

            if (IsNumber(new IEvaluable[] { first }).IsTrue() && IsNumber(new IEvaluable[] { second }).IsTrue())
                return AST.Symbol.Boolean(((AST.Number)first).Value.Equals(((AST.Number)second).Value));

            return IsEq(x);
        }

        public static IEvaluable IsEqual(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(x.ElementAt(0).ToString() == x.ElementAt(1).ToString());
        }

        #endregion

        #region Numeric Comparison Predicates

        public static IEvaluable IsZero(IEnumerable<IEvaluable> x) { return AST.Symbol.Boolean(((AST.Number)x.First()).Value.IsZero); }

        public static IEvaluable IsNumEqual(IEnumerable<IEvaluable> nums)
        {
            var n1 = (AST.Number)nums.First();
            var n2 = (AST.Number)nums.ElementAt(1);

            return AST.Symbol.Boolean(n1.Value.Equals(n2.Value));
        }
        public static IEvaluable IsGreater(IEnumerable<IEvaluable> nums)
        {
            var n1 = (AST.Number)nums.First();
            var n2 = (AST.Number)nums.ElementAt(1);

            return AST.Symbol.Boolean(n1.Value.Subtract(n2.Value).Sign > 0);
        }
        public static IEvaluable IsGreaterEqual(IEnumerable<IEvaluable> nums)
        {
            var n1 = (AST.Number)nums.First();
            var n2 = (AST.Number)nums.ElementAt(1);

            return AST.Symbol.Boolean(n1.Value.Subtract(n2.Value).Sign >= 0);
        }
        public static IEvaluable IsLess(IEnumerable<IEvaluable> nums)
        {
            var n1 = (AST.Number)nums.First();
            var n2 = (AST.Number)nums.ElementAt(1);

            return AST.Symbol.Boolean(n1.Value.Subtract(n2.Value).Sign < 0);
        }
        public static IEvaluable IsLessEqual(IEnumerable<IEvaluable> nums)
        {
            var n1 = (AST.Number)nums.First();
            var n2 = (AST.Number)nums.ElementAt(1);

            return AST.Symbol.Boolean(n1.Value.Subtract(n2.Value).Sign <= 0);
        }

        #endregion

        #region Environment Predicates

        public static IEvaluable HasEnvironmentParent(IEnumerable<IEvaluable> x)
        {
            var e = (Environment)x.First();
            return Symbol.Boolean(!e.IsGlobal);
        }

        public static IEvaluable EnvironmentBound(IEnumerable<IEvaluable> x)
        {
            var e = (Environment)x.First();
            var s = (Symbol)x.Skip(1).First();

            return Symbol.Boolean(e.AllEntries.Contains(s));
        }

        #endregion

        #region I/O Predicates

        public static IEvaluable IsInputPort(IEnumerable<IEvaluable> x)
        {
            var p = (Port)x.First();
            return Symbol.Boolean(p.Type == PortType.Input);
        }

        public static IEvaluable IsOutputPort(IEnumerable<IEvaluable> x)
        {
            var p = (Port)x.First();
            return Symbol.Boolean(p.Type == PortType.Output);
        }

        #endregion

        #endregion

        #region Environment Primitives

        public static IEvaluable EnvironmentParent(IEnumerable<IEvaluable> x)
        {
            var e = (Environment)x.First();
            return e.Parent;
        }

        public static IEvaluable EnvironmentBoundNames(IEnumerable<IEvaluable> x)
        {
            var e = (Environment)x.First();
            return Pair.CreateList(e.Entries);
        }

        public static IEvaluable EnvironmentBindings(IEnumerable<IEvaluable> x)
        {
            var e = (Environment)x.First();
            return Pair.CreateList(e.Entries.Select(s =>
            {
                try
                {
                    var v = e.Lookup(s);
                    return Pair.CreateList(new IEvaluable[] { s, v });
                }
                catch (SchemeRuntimeException)
                {
                    return new Pair(s, Pair.Empty);
                }
            }));
        }

        public static IEvaluable EnvironmentLookup(IEnumerable<IEvaluable> x)
        {
            var e = (Environment)x.First();
            return e.Lookup((Symbol)x.Skip(1).First());
        }

        public static IEvaluable EnvironmentAssign(IEnumerable<IEvaluable> x)
        {
            var e = (Environment)x.First();
            var s = (Symbol)x.Skip(1).First();
            var obj = x.Skip(2).First();

            e.SetValue(s, obj);
            return Symbol.Unspecified;
        }

        #endregion

        #region I/O Primitives

        public static IEvaluable WriteChar(IEnumerable<IEvaluable> x)
        {
            var c = (AST.Character)x.First();
            var p = (Port)x.Skip(1).First();
            p.Writer.Write(c.Value);

            return Symbol.Unspecified;
        }

        public static IEvaluable Read(IEnumerable<IEvaluable> x)
        {
            var p = (Port)x.First();
            var s = p.Reader.ReadLine();
            return Parser.SchemeParser.Parse(s);
        }

        public static IEvaluable Error(IEnumerable<IEvaluable> x)
        {
            if (x.Count() > 1)
            {
                var sb = new StringBuilder();
                sb.Append(x.First());
                foreach (var d in x.Skip(1))
                {
                    sb.Append(" ");
                    sb.Append(d);
                }
                throw new SchemeRuntimeException(sb.ToString());
            }
            else
                throw new SchemeRuntimeException(x.First().ToString());
        }

        public static IEvaluable CloseInputPort(IEnumerable<IEvaluable> x)
        {
            var p = (Port)x.First();
            if (p.Type == PortType.Input)
            {
                p.Close();
                return Symbol.Unspecified;
            }
            else
                throw new SchemeRuntimeException("Not an input port.");
        }

        public static IEvaluable CloseOutputPort(IEnumerable<IEvaluable> x)
        {
            var p = (Port)x.First();
            if (p.Type == PortType.Output)
            {
                p.Close();
                return Symbol.Unspecified;
            }
            else
                throw new SchemeRuntimeException("Not an output port.");
        }

        public static IEvaluable OpenInputFile(IEnumerable<IEvaluable> x)
        {
            var fname = ((AST.String)x.First()).Value;
            var reader = new System.IO.StreamReader(fname);

            return new Port(reader);
        }

        public static IEvaluable OpenOutputFile(IEnumerable<IEvaluable> x)
        {
            var fname = ((AST.String)x.First()).Value;
            var reader = new System.IO.StreamWriter(fname);

            return new Port(reader);
        }

        public static IEvaluable IsEofObject(IEnumerable<IEvaluable> x)
        {
            return Symbol.Boolean(x.First() is EofObject);
        }

        #endregion

        #region Pair Creation and Manipulation

        public static IEvaluable Car(IEnumerable<IEvaluable> x)
        {
            if (!x.First().IsPair())
                throw new SchemeRuntimeException(string.Format("{0} is not a pair", x.First()));
            var p = (Pair)x.First();
            return p.Car;
        }

        public static IEvaluable Cdr(IEnumerable<IEvaluable> x)
        {
            if (!x.First().IsPair())
                throw new SchemeRuntimeException(string.Format("{0} is not a pair", x.First()));
            var p = (Pair)x.First();
            return p.Cdr;
        }

        public static IEvaluable SetCar(IEnumerable<IEvaluable> x)
        {
            if (!x.First().IsPair())
                throw new SchemeRuntimeException(string.Format("{0} is not a pair", x.First()));
            var p = (Pair)x.First();
            p.Car = x.Skip(1).First();
            return Symbol.Unspecified;
        }

        public static IEvaluable SetCdr(IEnumerable<IEvaluable> x)
        {
            if (!x.First().IsPair())
                throw new SchemeRuntimeException(string.Format("{0} is not a pair", x.First()));
            var p = (Pair)x.First();
            p.Cdr = x.Skip(1).First();
            return Symbol.Unspecified;
        }

        public static IEvaluable Cons(IEnumerable<IEvaluable> x)
        {
            return new Pair(x.First(), x.ElementAt(1));
        }

        #endregion

        #region Vector Creation and Manipulation

        public static IEvaluable MakeVector(IEnumerable<IEvaluable> x) { return new AST.Vector(x); }

        public static IEvaluable VectorRef(IEnumerable<IEvaluable> x)
        {
            var v = (Vector)x.First();
            var r = (Scheme.Net.Integer)((AST.Number)x.Skip(1).First()).Value;

            return v.Values.ElementAt(r.FixValue.Value);
        }

        public static IEvaluable VectorLength(IEnumerable<IEvaluable> x)
        {
            var v = (Vector)x.First();
            return new AST.Number(Number.ParseInteger(v.Values.Count.ToString()));
        }

        public static IEvaluable VectorSet(IEnumerable<IEvaluable> x)
        {
            var v = (AST.Vector)x.First();
            var k = ((Integer)((AST.Number)x.Skip(1).First()).Value).FixValue.Value;
            var obj = x.Skip(2).First();

            v.Values[k] = obj;

            return Symbol.Unspecified;
        }

        public static IEvaluable VectorToList(IEnumerable<IEvaluable> x)
        {
            var v = (Vector)x.First();
            return Pair.CreateList(v.Values);
        }

        public static IEvaluable ListToVector(IEnumerable<IEvaluable> x)
        {
            var l = (Pair)x.First();
            if (l.IsDottedPair)
                throw new SchemeRuntimeException(string.Format("Not a proper list: {0}", l));
            return new Vector(l.Values);
        }

        #endregion

        #region Numbers

        public static IEvaluable Add(IEnumerable<IEvaluable> nums)
        {
            var n = nums.Select(v => ((AST.Number)v).Value).Aggregate(
                    Number.Parse("0"),
                    (n1, n2) => n1.Add(n2));
            return new AST.Number(n);
        }
        public static IEvaluable Multiply(IEnumerable<IEvaluable> nums)
        {
            var n = nums.Select(v => ((AST.Number)v).Value).Aggregate(
                    Number.Parse("1"),
                    (n1, n2) => n1.Multiply(n2));
            return new AST.Number(n);
        }
        public static IEvaluable Subtract(IEnumerable<IEvaluable> nums)
        {
            if (nums.Count() == 1)
                return new AST.Number(((AST.Number)nums.First()).Value.Negate());
            var n = nums.Select(v => ((AST.Number)v).Value).Aggregate(
                    (n1, n2) => n1.Subtract(n2));
            return new AST.Number(n);
        }
        public static IEvaluable Divide(IEnumerable<IEvaluable> nums)
        {
            if (nums.Count() == 1)
                return new AST.Number(Scheme.Net.Number.ParseInteger("1").Divide(((AST.Number)nums.First()).Value));
            var n = nums.Select(v => ((AST.Number)v).Value).Aggregate(
                    (n1, n2) => n1.Divide(n2));
            return new AST.Number(n);
        }

        public static IEvaluable Numerator(IEnumerable<IEvaluable> nums)
        {
            var n = ((AST.Number)nums.First()).Value;
            if (n is Rational)
                return new AST.Number(((Rational)n).Numerator);
            else
                return nums.First();
        }

        public static IEvaluable Denominator(IEnumerable<IEvaluable> nums)
        {
            var n = ((AST.Number)nums.First()).Value;
            if (n is Rational)
                return new AST.Number(((Rational)n).Denominator);
            else
                return new AST.Number(Number.ParseInteger("1"));
        }

        public static IEvaluable NumberToString(IEnumerable<IEvaluable> num)
        {
            return new AST.String(((AST.Number)num.First()).Value.ToString());
        }

        public static IEvaluable StringToNumber(IEnumerable<IEvaluable> str)
        {
            return new AST.Number(Number.Parse(((AST.String)str.First()).Value));
        }

        #endregion

        #region Symbols

        public static IEvaluable StringToSymbol(IEnumerable<IEvaluable> x)
        {
            var s = (AST.String)x.First();
            return Symbol.Lookup(s.Value);
        }

        public static IEvaluable SymbolToString(IEnumerable<IEvaluable> x)
        {
            var s = (Symbol)x.First();
            return new AST.String(s.Name);
        }

        #endregion

        #region Strings and Characters

        #region Character Ordering

        public static IEvaluable IsCharEq(IEnumerable<IEvaluable> x)
        {
            var first = (AST.Character)x.First();
            var second = (AST.Character)x.Skip(1).First();

            return Symbol.Boolean(first.Value == second.Value);
        }

        public static IEvaluable IsCharLess(IEnumerable<IEvaluable> x)
        {
            var first = (AST.Character)x.First();
            var second = (AST.Character)x.Skip(1).First();

            return Symbol.Boolean(first.Value < second.Value);
        }

        public static IEvaluable IsCharGreater(IEnumerable<IEvaluable> x)
        {
            var first = (AST.Character)x.First();
            var second = (AST.Character)x.Skip(1).First();

            return Symbol.Boolean(first.Value > second.Value);
        }

        public static IEvaluable IsCharLessEq(IEnumerable<IEvaluable> x)
        {
            var first = (AST.Character)x.First();
            var second = (AST.Character)x.Skip(1).First();

            return Symbol.Boolean(first.Value <= second.Value);
        }

        public static IEvaluable IsCharGreaterEq(IEnumerable<IEvaluable> x)
        {
            var first = (AST.Character)x.First();
            var second = (AST.Character)x.Skip(1).First();

            return Symbol.Boolean(first.Value >= second.Value);
        }

        public static IEvaluable CharToInteger(IEnumerable<IEvaluable> x)
        {
            var c = ((AST.Character)x.First()).Value;
            return new AST.Number(Number.ParseInteger(((int)c).ToString()));
        }

        public static IEvaluable IntegerToChar(IEnumerable<IEvaluable> x)
        {
            var i = ((Scheme.Net.Integer)((AST.Number)x.First()).Value).FixValue.Value;
            return new AST.Character((char)i);
        }

        #endregion

        #region Strings

        public static IEvaluable MakeString(IEnumerable<IEvaluable> x)
        {
            var c = '_';
            if (x.Count() > 1)
                c = ((AST.Character)x.Skip(1).First()).Value;
            var count = ((Integer)((AST.Number)x.First()).Value).FixValue.Value;
            return new AST.String(Enumerable.Repeat(c, count).Aggregate("", (a, s) => a + s));
        }

        public static IEvaluable StringFromChars(IEnumerable<IEvaluable> x)
        {
            var s = new string(x.Select(c => ((AST.Character)c).Value).ToArray());
            return new AST.String(s);
        }

        public static IEvaluable StringLength(IEnumerable<IEvaluable> x)
        {
            return new AST.Number(Number.ParseInteger(((AST.String)x.First()).Value.Length.ToString()));
        }

        public static IEvaluable StringRef(IEnumerable<IEvaluable> x)
        {
            var s = ((AST.String)x.First()).Value;
            var k = ((Integer)((AST.Number)x.Skip(1).First()).Value).FixValue.Value;
            return new AST.Character(s[k]);
        }

        public static IEvaluable StringSet(IEnumerable<IEvaluable> x)
        {
            var s = (AST.String)x.First();
            var k = ((Integer)((AST.Number)x.Skip(1).First()).Value).FixValue.Value;
            var c = ((AST.Character)x.Skip(2).First()).Value;

            var temp = s.Value.ToCharArray();
            temp[k] = c;
            s.Value = new string(temp);

            return Symbol.Unspecified;
        }

        public static IEvaluable StringAppend(IEnumerable<IEvaluable> x)
        {
            var stringList = x.Select(d => ((AST.String)d).Value);
            return new AST.String(stringList.Aggregate((a, s) => a + s));
        }

        public static IEvaluable Substring(IEnumerable<IEvaluable> x)
        {
            var s = ((AST.String)x.First()).Value;
            var start = ((Scheme.Net.Integer)((AST.Number)x.Skip(1).First()).Value).FixValue.Value;
            var end = ((Scheme.Net.Integer)((AST.Number)x.Skip(2).First()).Value).FixValue.Value;

            return new AST.String(s.Substring(start, end - start));
        }

        #endregion

        #endregion
    }
}
